// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.control.physics;

import bats.control.BatsControl;
import bats.scene.SceneNode;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.objects.PhysicsRigidBody;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.Control;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3utilities.MyString;
import jme3utilities.physics.OverlapListener;
import jme3utilities.physics.SimpleGhostControl;

/**
 * A ghost physics control used to grow the collision shape of a rigid body or
 * when adding a rigid body to a location which might be obstructed. The control
 * waits for the shape to become unobstructed and then enables any "successor"
 * controls, disables any "timeout" controls, and disables itself.
 * <p>
 * Each instance is enabled by at creation.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class GrowControl
        extends SimpleGhostControl
        implements OverlapListener {
    // *************************************************************************
    // constants

    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(GrowControl.class.getName());
    // *************************************************************************
    // fields
    /**
     * true if the collision shape is clear so far, false otherwise
     */
    private boolean clearSoFar = false;
    /**
     * true if there's been a physics tick since the last update
     */
    private boolean hasTicked = false;
    /**
     * list of successor controls to be enabled when the collision shape is
     * unobstructed
     */
    final private List<Control> successors = new ArrayList<>();
    /**
     * list of timeout controls to be disabled when the collision shape is
     * unobstructed
     */
    final private List<Control> timeouts = new ArrayList<>();
    /**
     * which scene contains the spatial: set by constructor
     */
    final private SceneNode scene;
    // *************************************************************************
    // constructors

    /**
     * Instantiate an enabled control for the specified shape and scene, with no
     * successors and no timeouts.
     *
     * @param shape initial collision shape (not null)
     * @param scene which scene the object is in (not null)
     */
    public GrowControl(CollisionShape shape, SceneNode scene) {
        super(true, shape, scene.getWorld().getPhysics().getSpace());
        assert shape != null;
        this.scene = scene;
        assert isEnabled();
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Add a successor control which will be enabled when the shape becomes
     * unobstructed.
     *
     * @param control
     */
    public void addSuccessor(Control control) {
        assert BatsControl.isValid(control);
        successors.add(control);
    }

    /**
     * Add a timeout control which will be disabled when the shape becomes
     * unobstructed.
     *
     * @param control
     */
    public void addTimeout(Control control) {
        assert BatsControl.isValid(control);
        timeouts.add(control);
    }
    // *************************************************************************
    // GhostControl methods

    /**
     * Update the control. (Invoked once per frame.)
     *
     * @param simInterval simulation seconds since the previous update (&gt;0)
     */
    @Override
    public void update(float simInterval) {
        super.update(simInterval);
        if (spatial != null && isEnabled() && scene.isEnabled() && hasTicked) {
            if (clearSoFar) {
                /*
                 * No overlaps found (in physics space).
                 */
                allClear();
            } else {
                logger.log(Level.INFO, "{0} remains obstructed",
                        MyString.quote(spatial.getName()));
            }
        }
        hasTicked = false;
        clearSoFar = true;
    }
    // *************************************************************************
    // OverlapListener methods

    /**
     * React to an overlapping rigid body.
     *
     * @param overlappingBody the overlapping rigid body (not null)
     * @param overlappingSpatial the spatial of the overlapping rigid body (not
     * null)
     * @param localPoint the location of the overlap (rotated and translated to
     * this control's object, but at world scale)
     */
    @Override
    public void onOverlap(PhysicsRigidBody overlappingBody,
            Spatial overlappingSpatial, Vector3f localPoint) {
        assert overlappingBody != null;
        assert overlappingSpatial != null;
        assert localPoint != null;

        if (!isEnabled()) {
            return;
        }
        logger.log(Level.FINE, "{0} overlaps with {1}", new Object[]{
            spatial.getName(),
            overlappingSpatial.getName()
        });
        clearSoFar = false;
    }
    // *************************************************************************
    // PhysicsGhostObject methods

    /**
     * Alter the collision shape of the physics object.
     *
     * @param collisionShape
     */
    @Override
    public void setCollisionShape(CollisionShape collisionShape) {
        super.setCollisionShape(collisionShape);
        clearSoFar = false;
    }
    // *************************************************************************
    // SimpleGhostControl methods

    /**
     * Method invoked after each physics tick.
     *
     * @param unused
     * @param unused2
     */
    @Override
    public void physicsTick(PhysicsSpace unused, float unused2) {
        if (spatial == null || !isEnabled() || !scene.isEnabled()) {
            return;
        }

        hasTicked = true;
    }
    // *************************************************************************
    // private methods

    /**
     * Enable any successor controls, disable any timeout controls, and disable
     * this control.
     */
    private void allClear() {
        logger.log(Level.INFO, "{0} all clear", spatial.getName());
        /*
         * Enable each successor control.
         */
        for (Control successor : successors) {
            BatsControl.setEnabled(successor, true);
            if (successor instanceof RigidBodyControl) {
                /*
                 * Copy the collision shape to each RigidBodyControl.
                 */
                RigidBodyControl rigidBody = (RigidBodyControl) successor;
                space.remove(rigidBody);
                rigidBody.setCollisionShape(collisionShape);
                space.add(rigidBody);
            }
        }
        successors.clear();
        /*
         * Disable each timeout control.
         */
        for (Control timeout : timeouts) {
            BatsControl.setEnabled(timeout, false);
        }
        timeouts.clear();

        setEnabled(false);
    }
}